package au.edu.unsw.editingtool.fedora;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import org.apache.axis.types.NonNegativeInteger;
import au.edu.unsw.editingtool.global.EditingToolException;
import fedora.client.FedoraClient;
import fedora.server.access.FedoraAPIA;
import fedora.server.management.FedoraAPIM;
import fedora.server.types.gen.ComparisonOperator;
import fedora.server.types.gen.Condition;
import fedora.server.types.gen.Datastream;
import fedora.server.types.gen.FieldSearchQuery;
import fedora.server.types.gen.FieldSearchResult;
import fedora.server.types.gen.ObjectFields;

public class FedoraFactory {
    
	private FedoraClient fed;
    private FedoraAPIM apim;
    private FedoraAPIA apia;
    private String token = "";
    private StringBuffer resultList = new StringBuffer(
    "<?xml version=\"1.0\" encoding=\"utf-8\"?>");
    
    public FedoraFactory(String fedoraStub, String username, String password) {
        try {
            fed = new FedoraClient(fedoraStub, username, password);
            apim = fed.getAPIM();
            apia = fed.getAPIA();
            this.token = "";
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public String modifyDatastreamByValue(String pidV, String datastreamID,
        String[] altID, String label, String contentType, String formatURI, 
        byte[] content, String checksumType, String checksum, 
        String log, Boolean force) throws IOException //throws EditingToolException 
    {    		
    	String result = null;
            try {
            	/* param:
            	 * String 	pid
            	 * String 	dsid
            	 * String[] altIDs
            	 * String	label
            	 * String	MimeType
            	 * String	formatUri
            	 * byte		content
            	 * String	ChecksumType
            	 * String	Checksum
            	 * String	logMessage
            	 * Boolean	force 
            	 */            	             	 
                //return apim.modifyDatastreamByValue(pidV, datastreamID, altID,
                //label, contentType, null, content, null, null, log, false);
            	result = apim.modifyDatastreamByValue(pidV, datastreamID, altID,
            			label, contentType, formatURI, 
                        content, checksumType, checksum, log, false);            	
            }
            catch (Exception e) {
            	
                //EditingToolException ete = new EditingToolException();
                //ete.setMessage("Failed modifying Datastream by value for PID: " + 
                //				pidV + 
                //				" and datastream ID: " + datastreamID);
                //ete.printStackTrace();                
                //System.out.println(ete.sendMessage());
                //throw ete;
            	e.printStackTrace();
            }
            return result.toString();
    }
    
    public String purgeObject(String pid, String log) throws EditingToolException {
        try {

            return apim.purgeObject(pid, log, false);
        } catch (Exception e) {
        	EditingToolException ete = new EditingToolException();
        	ete.setMessage("Failed purging object: " + e.toString());
        	ete.printStackTrace();
            throw ete;
        }
    }

    public ObjectFields[] searchNextResults(String tokenNew) throws EditingToolException {
        try {
            FieldSearchResult objects = apia.resumeFindObjects(tokenNew);
            ObjectFields[] results = objects.getResultList();
            try {
                this.token = objects.getListSession().getToken();
            } catch (NullPointerException exp) {
                this.token = "";
            }
            return results;
        } catch (Exception e) {
        	EditingToolException ete = new EditingToolException();
        	ete.setMessage("Failed searching for resumption token: " + token);
            throw ete;
        }
    }

    public ObjectFields[] searchResults(String term, String queryTag, int mResult) {
        try {

            FieldSearchQuery query = new FieldSearchQuery();
            Condition[] tempCond = new Condition[] { new Condition(queryTag,
                ComparisonOperator.has, term) };

            query.setConditions(tempCond);

            FieldSearchResult objects = apia.findObjects(new String[] { "pid",
                "title", "state", "label" }, new NonNegativeInteger ( "" + mResult ), query);

            ObjectFields[] results = objects.getResultList();
            try {
                this.token = objects.getListSession().getToken();
            } catch (NullPointerException exp) {
                this.token = "";
            }
            return results;

        } catch (Exception exp2) {
        	EditingToolException exception = new EditingToolException(
                "Failed searching for: queryTag: " + queryTag
                    + " with queryValue: " + term, exp2.toString());
            throw exception;
        }

    }

    public void createResultList(ObjectFields[] results) {
        try {
            resultList.append("<Result>" + System.getProperty("line.separator"));
            for (int i = 0; i < results.length; i++) {
                resultList.append("<item>"
                    + System.getProperty("line.separator"));

                resultList.append("<pid>"
                    + System.getProperty("line.separator"));
                resultList.append(results[i].getPid()
                    + System.getProperty("line.separator"));
                resultList.append("</pid>"
                    + System.getProperty("line.separator"));

                resultList.append("<title>"
                    + System.getProperty("line.separator"));
                resultList.append(results[i].getTitle(0)
                    + System.getProperty("line.separator"));
                resultList.append("</title>"
                    + System.getProperty("line.separator"));

                /*
                 * resultList.append("<creator>"+
                 * System.getProperty("line.separator"));
                 * resultList.append(results[i].getCreator(0)+
                 * System.getProperty("line.separator"));
                 * resultList.append("</creator>"+
                 * System.getProperty("line.separator"));
                 * 
                 * resultList.append("<type>"+
                 * System.getProperty("line.separator"));
                 * resultList.append(results[i].getType(0)+
                 * System.getProperty("line.separator"));
                 * resultList.append("</type>"+
                 * System.getProperty("line.separator"));
                 * 
                 * resultList.append("<date>"+
                 * System.getProperty("line.separator"));
                 * resultList.append(results[i].getDate(0)+
                 * System.getProperty("line.separator"));
                 * resultList.append("</date>"+
                 * System.getProperty("line.separator"));
                 */
                resultList.append("<state>"
                    + System.getProperty("line.separator"));
                resultList.append(results[i].getState()
                    + System.getProperty("line.separator"));
                resultList.append("</state>"
                    + System.getProperty("line.separator"));

                resultList.append("</item>"
                    + System.getProperty("line.separator"));
            }
            resultList.append("</Result>"
                + System.getProperty("line.separator"));
        } catch (Exception exp2) {
            EditingToolException exception = new EditingToolException(
                "Failed creating result list", exp2.toString());
            throw exception;
        }
    }

    public byte[] getDatastream(String pidV, String datastreamId)
        throws EditingToolException, TransformerException,
        TransformerConfigurationException, FileNotFoundException, IOException {

        try {
            return apia.getDatastreamDissemination(pidV, datastreamId, null)
                .getStream();

        } catch (Exception e) {
        	EditingToolException ete = new EditingToolException();
            ete.setMessage("Failed getting Datastream with ID " + 
            				datastreamId + " and PID " + pidV);
            throw ete;
        }

    }
    
   public Datastream getDatastreamInfo(String pidV, String datastreamId) {
    	try {
			return apim.getDatastream(pidV, datastreamId, null);
		} catch (RemoteException e) {
			e.printStackTrace();
			EditingToolException ete = new EditingToolException();
            ete.setMessage("Failed getting Datastream Infomation with ID " + 
            				datastreamId + " and PID " + pidV);
            throw ete;
		}
    }
    
    public String getDatastreamVersionID(String pidV, String datastreamId) {
    	Datastream ds = this.getDatastreamInfo(pidV, datastreamId);
    	return ds.getVersionID();
    }

    public List getDatastreamIds(String pidValue) {
        try {
            Datastream[] list = apim.getDatastreams(pidValue, null, null);
            return Arrays.asList(list);
        } catch (Exception exp) {
            EditingToolException exception = new EditingToolException(
                "Failed getting Datastreams with pid " + pidValue,
                exp.toString());
            throw exception;
        }

    }

    public List getDatastreamIds(String pidValue, String state)
        throws EditingToolException, TransformerException,
        TransformerConfigurationException, FileNotFoundException, IOException {

        try {
            Datastream[] list = apim.getDatastreams(pidValue, null, null);
            int length = list.length;
            List listnames = new ArrayList();
            for (int i = 0; i < length; i++) {
                if (list[i].getState().compareTo(state) == 0) {
                    listnames.add(list[i].getID());
                }
            }
            return listnames;
        } catch (Exception exp) {
            EditingToolException exception = new EditingToolException(
                "Failed getting Datastreams with pid " + pidValue,
                exp.toString());
            throw exception;
        }

    }

    public byte[] transformXML(byte[] sourcestream, String stylesheet) {
    	byte[] XMLbyte = null;
        try {

            InputStream source = new ByteArrayInputStream(sourcestream);
            StringWriter strWriter = new StringWriter();
            StreamResult result = new StreamResult(strWriter);

            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer(new StreamSource(
                stylesheet));
            transformer.transform(new StreamSource(source), result);

            String xmlAsString = strWriter.getBuffer().toString();
            source.close();
            strWriter.close();
            XMLbyte = xmlAsString.getBytes("UTF-8");
            //System.out.println("NEWXML: "+xmlAsString);
            
        } catch (Exception exp) {
           // EditingToolException exception = new EditingToolException(
            //    "Failed transforming source stream", exp.toString());
           // throw exception;
        	exp.printStackTrace();
        }
        return XMLbyte;
    }

    public String getToken() {
        return this.token;
    }

    public byte[] writeFoxml(String sourceId, String sourceLabel,
        String source, String dc, String label, String state) {
        StringBuffer foxml = new StringBuffer();

        foxml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        foxml.append("<foxml:digitalObject xmlns:foxml=\"info:fedora/fedora-system:def/foxml#\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"info:fedora/fedora-system:def/foxml# http://www.fedora.info/definitions/1/0/foxml1-0.xsd\">");
        foxml.append("<foxml:objectProperties>");
        foxml.append("<foxml:property NAME=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\" VALUE=\"FedoraObject\"></foxml:property>");
        foxml.append("<foxml:property NAME=\"info:fedora/fedora-system:def/model#state\" VALUE=\""
            + state + "\"></foxml:property>");
        foxml.append("<foxml:property NAME=\"info:fedora/fedora-system:def/model#label\" VALUE=\""
            + label + "\"></foxml:property>");
        foxml.append("</foxml:objectProperties>");
        foxml.append("<foxml:datastream CONTROL_GROUP=\"X\" ID=\"DC\" STATE=\"A\" VERSIONABLE=\"true\">");
        foxml.append("<foxml:datastreamVersion ID=\"DC.0\" LABEL=\"Dublin Core\" MIMETYPE=\"text/xml\">");
        foxml.append("<foxml:xmlContent>");
        dc = dc.substring(38);
        foxml.append(dc);
        foxml.append("</foxml:xmlContent>");
        foxml.append("</foxml:datastreamVersion>");
        foxml.append("</foxml:datastream>");

        foxml.append("<foxml:datastream CONTROL_GROUP=\"X\" ID=\"" + sourceId
            + "\" STATE=\"I\" VERSIONABLE=\"true\">");
        foxml.append("<foxml:datastreamVersion ID=\"" + sourceId
            + ".0\" LABEL=\"" + sourceLabel + "\" MIMETYPE=\"text/xml\">");
        foxml.append("<foxml:xmlContent>");
        source = source.substring(39);
        foxml.append(source);
        foxml.append("</foxml:xmlContent>");
        foxml.append("</foxml:datastreamVersion>");
        foxml.append("</foxml:datastream>");

        foxml.append("</foxml:digitalObject>");
        return foxml.toString().getBytes();
    }

    public String getNextPid(String namespace) {

        try {
            return apim.getNextPID(new NonNegativeInteger( "1" ), namespace)[0];
        } catch (Exception exp) {
            EditingToolException exception = new EditingToolException(
                "Failed getting next available PID for namespace:" + namespace,
                exp.toString());
            throw exception;
        }

    }

    public String setDatastreamState(String pidValue, String itemIdValue,
        String stateValue, String logValue) {
        try {

            return apim.setDatastreamState(pidValue, itemIdValue, stateValue,
                logValue);
        } catch (Exception exp) {
            EditingToolException exception = new EditingToolException(
                "Failed re-setting the state of datastreamID: " + itemIdValue
                    + " for PID:" + pidValue, exp.toString());
            throw exception;
        }

    }

    public String processNewRecord(String pidNamespace, String sourceId,
        String sourceLabel, byte[] source, String stylesheetPath, String label,
        String state) {
        try {
            // String pid = getNextPid(pidNamespace);

            byte[] tempDc = transformXML(source, stylesheetPath);
            String dc = new String(tempDc);
            // remove xml syntax out of the newly created xml
            dc = dc.substring(39);

            return ingestObject(writeFoxml(sourceId, sourceLabel, new String(
                source), dc, label, state));

        } catch (Exception exp) {
            EditingToolException exception = new EditingToolException(
                "Failed Processing New Record", exp.toString());
            throw exception;
        }
    }

    // public String processModifiedRecord(String pidLocal, String marcxml,
    // String stylesheetPath, String log) {
    // try {
    //
    // String timeM = modifyDatastreamByValue(pidLocal, this.marcItemID,
    // new String[] { this.marcAlternateID }, "Marc21 xml",
    // marcxml.getBytes("UTF-8"), "I", log);
    // String timeD = modifyDatastreamByValue(pidLocal, this.dCItemID,
    // new String[] {}, "Dublin Core", marc2dcTransform2(
    // stylesheetPath, marcxml, pidLocal), "A", log);
    //
    // return "successfully modified object: " + pidLocal
    // + " with timestamp: " + timeM + " and " + timeD;
    //
    // } catch (Exception exp) {
    // EditingToolException exception = new EditingToolException(
    // "Failed Processing New Record", exp.toString());
    // throw exception;
    // }
    // }

    public String ingestObject(byte[] foxml) {

        try {
            return apim.ingest(foxml, "foxml1.0",
                "Object added using Fabulous version 3.0");
        } catch (Exception exp) {
            EditingToolException exception = new EditingToolException(
                "Failed ingesting Object", exp.toString());
            throw exception;
        }

    }

    public String getHandle(String pidLocal, String datastreamID, String prefix)
        throws RuntimeException, org.xml.sax.SAXException,
        javax.xml.parsers.ParserConfigurationException, IOException {
        String handle = "";
        try {

            byte[] dc = getDatastream(pidLocal, datastreamID);
            InputStream input = new ByteArrayInputStream(dc);

            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document sourceDoc = builder.parse(input);
            NodeList identifiers = sourceDoc.getElementsByTagName("dc:identifier");
            for (int i = 0; i < identifiers.getLength(); i++) {
                // String temp=identifiers.item(i).getTextContent().trim();
                try {
                    String temp = identifiers.item(i)
                        .getChildNodes()
                        .item(0)
                        .getNodeValue();

                    if (temp.contains(prefix)) {
                        handle = temp;
                        break;
                    }
                } catch (Exception E) {

                }
            }

        } catch (Exception exp2) {
            RuntimeException exception = new RuntimeException(
                "ERROR: Failed getting Handle for datastream ID "
                    + datastreamID + " and PID " + pidLocal
                    + " and handle prefix " + prefix + " INFO: "
                    + exp2.toString());
            throw exception;
        }
        return handle;
    }

    public String xmlValidate(String xml, String schemaPath) {
        try {
            // parse an XML document into a DOM tree
            DocumentBuilder parser = DocumentBuilderFactory.newInstance()
                .newDocumentBuilder();
            Document document = parser.parse(new InputSource(new StringReader(
                xml)));

            // create a SchemaFactory capable of understanding WXS schemas
            SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");

            // load a WXS schema, represented by a Schema instance
            // Source schemaFile = new StreamSource(new File("MARC21slim.xsd"));

            InputStream IS = Thread.currentThread()
                .getContextClassLoader()
                .getResourceAsStream(schemaPath);
            Source schemaFile = new StreamSource(IS);
            Schema schema = factory.newSchema(schemaFile);

            // create a Validator instance, which can be used to validate an
            // instance document
            Validator validator = schema.newValidator();

            // validate the DOM tree

            validator.validate(new DOMSource(document));
            return xml;
        } catch (Exception exp) {
            // instance document is invalid!
            EditingToolException exception = new EditingToolException(
                    "Validation Failed", exp.toString());
            throw exception;
        }

    }

    public String getLabel(String pid) {
        ObjectFields[] obj = searchResults(pid, "pid", 1);
        if (obj != null && obj.length > 0) {
            return obj[0].getLabel();
        }
        return null;
    }

    public void setLabel(String pid, String newLabel, String state) {
        try {
            apim.modifyObject(pid, state, newLabel, "fedoraAdmin",
                "changed stage");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    
	
}
